import sys
import traceback
import os
import platform
import time
import subprocess
import optparse
import multiprocessing
import shlex

from made.util import ipc
from made.util import pidfile
from made.util import command
from made.util import daemon
from made.util import misc


def run_client(group_name, max_jobs):
    ran_a_command = multiprocessing.Value("b", False)
    client_return_code = multiprocessing.Value("i", 0)
    args = (group_name, ran_a_command, client_return_code)

    if max_jobs > 1:
        # We can't just have the first process start the server.  Otherwise,
        # if we're not creating a persistent server, the first process will
        # finish because there are no more tasks to run and the server will
        # exit along with it.  The other processes will then generate errors
        # when trying to connect to the server.
        #
        # Instead, we connect to the server now to ensure it is started from
        # this process.
        client_return_code.value = load_madefiles()
        if client_return_code.value != 0:
            return client_return_code.value

        processes = [
            multiprocessing.Process(target=run_client_process, args=args)
            for process_index in range(max_jobs)]

        for process in processes:
            process.start()

        for process in processes:
            process.join()

        # TODO: Send the server a shutdown from this process now if we're
        #       not starting a persistent server?  This way, the server will
        #       shut down cleanly.
    else:
        run_client_process(*args)

    if not ran_a_command.value:
        on_display_message("Nothing to do")

    return client_return_code.value

def run_client_process(group_name, ran_a_command, client_return_code):
    original_dir = _get_unix_pwd()

    # If another process failed then stop running this one.
    while client_return_code.value == 0:
        # TODO: Handle signals and recover gracefully, sending a response back.

        # Ask for a command to run, stopping if there isn't one.  On Windows
        # we'll get json errors trying to serialize os.environ since it's not a
        # regular dictionary, so we convert it to one.
        command_info = send_and_read_response({
            "type": "get_command",
            "dir": original_dir,
            "group_name": group_name,
            "pid": os.getpid(),
            "environ": dict(os.environ)})

        if "error" in command_info:
            on_display_message(command_info["error"], is_error=True)
            client_return_code.value = 1
            break

        # If there's nothing left to run then stop.  However, if there are
        # other clients running jobs under this directory, don't exit this
        # client if there's nothing to run yet, since jobs may become
        # available.  Note that the active client may be a multiprocessing
        # sibling process or it could be unrelated.
        if "commands" not in command_info:
            if "active_client_pid" not in command_info:
                break

            time.sleep(0)
            continue

        return_code, return_value = run_commands(
            [command.from_dict(command_string)
                for command_string in command_info["commands"]],
            command_info["dir"])
        ran_a_command.value = True

        # Tell the server that we're done running the command.
        # TODO: Handle the case where the server shut down between when we
        #       received the command to run and now.
        reply = send_and_read_response({
            "type": "command_results",
            "pid": os.getpid(),
            "return_code": return_code,
            "return_value": return_value})

        if "error" in reply:
            on_display_message(reply["error"], is_error=True)
            client_return_code.value = 1
            break

        if return_code != 0:
            client_return_code.value = return_code

def send_and_read_response(message, connection=None):
    if connection is None:
        connection = connect_to_server()

    ipc.send_over_connection(connection, message)
    reply = ipc.read_from_connection(connection)
    ipc.close_connection(connection)

    return reply

_connection_lock = multiprocessing.RLock()

# We need to make sure simultaneous clients don't all try to start the server
# at the same time.
@misc.globally_locked(_connection_lock)
def connect_to_server(start_if_not_running=True):
    if pidfile.is_pid_file_valid():
        connection = ipc.open_connection()
        assert(connection is not None)
        return connection

    if not start_if_not_running:
        return None

    if _start_persistent_server:
        _start_background_server_process()
    else:
        _fork_server_process()
    connection = ipc.wait_for_and_open_connection()
    assert(connection)
    return connection

def _start_background_server_process():
    import made.server
    try:
        daemon.start_detached_program(("made", "-s", "-f"))
    except OSError:
        on_display_message("could not start madeserver")
        sys.exit(1)

def _fork_server_process():
    import made.server
    process = multiprocessing.Process(
        target=made.server.start_server_as_daemon)
    process.daemon = True
    process.start()

def _get_unix_pwd():
    return os.getcwd().replace("\\", "/")

def run_commands(commands, directory):
    if _get_unix_pwd() != directory:
        on_display_message("entering %s" % directory)
        os.chdir(directory)

    return_value = None
    for command in commands:
        on_command_start(command.command, command.description, os.getpid())
        return_code, return_value = command.run()
        on_command_done(return_code)

        if return_code != 0:
            return return_code, return_value

    return 0, return_value

def load_madefiles():
    response = send_and_read_response({
        "type": "load_madefile",
        "dir": _get_unix_pwd(),
        "environ": dict(os.environ)})

    if "error" not in response:
        return 0

    on_display_message(response["error"], is_error=True)
    return 1

def quit_server():
    connection = connect_to_server(start_if_not_running=False)
    if connection is None:
        on_display_message("The server is not running")
        return

    send_and_read_response({"type": "quit"}, connection)

def generate_dependencies_image(image_file_name, group_name):
    # On Windows we'll get json errors trying to serialize os.environ since
    # it's not a regular dictionary, so we convert it to one.
    response = send_and_read_response({
        "type": "dump_task_graph",
        "dir": _get_unix_pwd(),
        "group_name": group_name,
        "image_file_path": os.path.abspath(image_file_name),
        "environ": dict(os.environ)})

    if "error" not in response:
        return 0

    on_display_message(response["error"], is_error=True)
    return 1

# TODO: Make these functions callbacks that can be overridden by graphical
#       clients.
def on_display_message(message, is_error=False):
    # TODO: Add an option to suppress all non-error messages (e.g. entering
    #       directory, nothing to do, etc.)
    stream = (sys.stderr if is_error else sys.stdout)
    stream.write((message if is_error else "[made] " + message) + "\n")
    stream.flush()

def on_command_start(command, description, pid):
    # TODO: Add an option to just display the short description.
    # TODO: Add an option to display the pid.
    sys.stdout.write(command + "\n")
    sys.stdout.flush()

# TODO: This function is not used.  If we want to use it, we need to capture
#       the command's output via a pipe.
def on_command_output(output):
    sys.stdout.write(output)
    sys.stdout.flush()

def on_command_done(return_code):
    """Graphical clients might use this information to flag commands with
    errors.
    """
    pass

def run_command_line_client(options, args):
    """Run the command-line version of the client, printing output to the
    shell.
    """
    if options.quit_server:
        quit_server()
        sys.exit(0)

    group_name = (args[0] if len(args) != 0 else "default")

    if options.graphviz_output:
        return_code = generate_dependencies_image(
            options.graphviz_output, group_name)
    elif options.load_madefiles:
        return_code = load_madefiles()
    else:
        return_code = run_client(group_name, options.max_jobs)

    sys.exit(return_code)

def create_option_parser():
    parser = optparse.OptionParser(usage="%prog [options] [task-group-name]")
    parser.add_option(
        "-q", "--quit", dest="quit_server", action="store_true",
        help="quit server")
    parser.add_option(
        "-l", "--load-madefiles", dest="load_madefiles", action="store_true",
        help="load the madefile for the project in the current directory")
    parser.add_option(
# TODO: Restore the number of threads to num_procs, or possibly 8.
# TODO: Only launch new threads after getting a command, to the maximum.
#        "-j", "--max-jobs", dest="max_jobs", type="int", default=8,
        "-j", "--max-jobs", dest="max_jobs", type="int", default=1,
        help="maximum number of threads")
    parser.add_option(
        "-g", "--graphviz-output", dest="graphviz_output", default=None,
        metavar="IMAGE-FILE",
        help="generate an image of the dependencies")
    parser.add_option(
        "-f", "--foreground", dest="foreground", default=False,
        action="store_true",
        help="start the server without forking to the background")

    return parser

if __name__ == "__main__":
    parser = create_option_parser()
    options, args = parser.parse_args()

    if len(args) > 1:
        parser.error("You many only specify one task group name")

    global _start_persistent_server
    _start_persistent_server = not options.foreground

    run_command_line_client(options, args)
